
/**
 * @fileoverview This is a simple array collection which exposes binding events.
 * This collection will only manage eventdispatchers which are of the type
 * basemodel.
 */
goog.provide('lfpug.model.ArrayCollection');
goog.require('goog.events.EventTarget');
goog.require('goog.ui.IdGenerator');
goog.require('lfpug.events.ArrayCollectionEvent');
goog.require('lfpug.model.BaseModel');
goog.require('lfpug.model.BaseModel.EventType');



/**
 * @constructor
 * @extends {goog.events.EventTarget}
 *
 * @param {Array.<lfpug.model.BaseModel>=} opt_source
 *        The innitial items in the array collection.
 *
 * @param {string=} opt_id
 *        If you want to pass in an optional id instead of auto generating one.
 */
lfpug.model.ArrayCollection = function(opt_source, opt_id) {

  goog.base(this);

  if (goog.isDefAndNotNull(opt_id)) {
    this.setId(opt_id);
  } else {
    this.setId(goog.ui.IdGenerator.getInstance().getNextUniqueId());
  }

  /**
   * @type {Function}
   * @private
   */
  this.childListenerFunc_ = goog.bind(this.propegateEventInternal_, this);

  /**
   * @private
   * @type {Array.<lfpug.model.BaseModel.EventType>}
   * The events this array collection will bubble through the system.
   */
  this.childObservableEvents_ = [
    lfpug.model.BaseModel.EventType.CHANGE
  ];

  if (!goog.isDefAndNotNull(opt_source)) {
    /**
     * @type {!Array}
     * @private
     */
    this.source_ = [];
  } else {
    this.source_ = opt_source;
    for (var i = 0; i < opt_source.length; i++) {
      /**@type {!lfpug.model.BaseModel}*/
      var o = /**@type {!lfpug.model.BaseModel}*/ opt_source[i];
      this.addListeners_(
          this.childObservableEvents_, o, this.childListenerFunc_);

    }
  }
};
goog.inherits(lfpug.model.ArrayCollection, goog.events.EventTarget);


/**
 * @return {string} The UUID which I use to attach elements to the dom with.
 */
lfpug.model.ArrayCollection.prototype.getUUID = function() {
  return this.id_;
};


/**
 * a simple proxy to avoid having to manage event keys myself yet still listen
 * to an array of events
 * @private
 * @param {Array.<string>} events The events you want to listen too.
 * @param {lfpug.model.BaseModel} object The object you are adding the
 * listeners too.
 * @param {Function} handler The handler for the event listener.
 */
lfpug.model.ArrayCollection.prototype.addListeners_ =
    function(events, object, handler) {
  for (var i = 0; i < events.length; i++) {
    object.addEventListener(events[i], handler);
  }
};


/**
 * a simpler proxy to avoid having to manage event keys myself yet still listen
 * to an array of events
 * @private
 * @param {Array.<string>} events The events you want to listen too.
 * @param {lfpug.model.BaseModel} object The object you are adding the
 * listeners too.
 * @param {Function} handler The handler for the event listener.
 */
lfpug.model.ArrayCollection.prototype.removeListeners_ =
    function(events, object, handler) {
  for (var i = 0; i < events.length; i++) {
    object.removeEventListener(events[i], handler);
  }
};


/**
 * @param {!lfpug.model.BaseModel} item The item you want to add.
 */
lfpug.model.ArrayCollection.prototype.addItem = function(item) {
  this.addItemAt(item, this.source_.length);
};


/**
 * @param {!lfpug.model.BaseModel} item The item you want to add.
 * @param {!number} index The index at which you wish to add the item.
 */
lfpug.model.ArrayCollection.prototype.addItemAt = function(item, index) {
  goog.array.insertAt(this.source_, item, index);
  this.addListeners_(this.childObservableEvents_, item,
      this.childListenerFunc_);
  var event = new lfpug.events.ArrayCollectionEvent(
      lfpug.events.ArrayCollectionEvent.EventType.ADD, item);
  event.setItemIndex(index);
  this.dispatchEvent(event);
};


/**
 * @param {lfpug.model.BaseModel} item The item you wish to remove.
 */
lfpug.model.ArrayCollection.prototype.removeItem = function(item) {
  var index = goog.array.indexOf(this.source_, item);
  if (index >= 0 && index < this.getLength()) {
    this.removeItemAt(index);
  }
};


/**
 *
 * @param {number} oldI The index currently.
 * @param {number} newI The index it is being moved too.
 *
 */
lfpug.model.ArrayCollection.prototype.moveItem = function(oldI, newI) {
  if (goog.isDefAndNotNull(this.source_[oldI])) {
    var item = this.source_[oldI];
    this.source_[oldI] = this.source_[newI];
    this.source_[newI] = item;
    var event = new lfpug.events.ArrayCollectionEvent(
        lfpug.events.ArrayCollectionEvent.EventType.MOVE, item);
    event.setOldItem(this.source_[oldI]);
    event.setItemIndex(newI);
    event.setOldItemIndex(oldI);
    this.dispatchEvent(event);
  }
};


/**
 * @param {number} index The index at which you wish to remove the item.
 */
lfpug.model.ArrayCollection.prototype.removeItemAt = function(index) {
  if (index >= 0 && index < this.getLength()) {
    var item = /** @type {!lfpug.model.BaseModel}*/this.source_[index];
    this.source_.splice(index, 1);
    this.removeListeners_(this.childObservableEvents_, item,
        this.childListenerFunc_);
    this.dispatchEvent(new lfpug.events.ArrayCollectionEvent(
        lfpug.events.ArrayCollectionEvent.EventType.REMOVE, item));
  }
};


/**
 * Removes all of the items from this ArrayCollection
 */
lfpug.model.ArrayCollection.prototype.removeAll = function() {
  var len = this.source_.length;
  for (var i = 0; i < len; i++) {
    var item = /** @type {!lfpug.model.BaseModel}*/this.source_[i];
    this.removeListeners_(this.childObservableEvents_, item,
        this.childListenerFunc_);
  }
  this.source_ = [];
  this.dispatchEvent(new lfpug.events.ArrayCollectionEvent(
      lfpug.events.ArrayCollectionEvent.EventType.REMOVE_ALL));
};


/**
 * @param {number} index The index at which you wish to get the item.
 * @return {?lfpug.model.BaseModel} Returns the object if the index is a
 * sane.
 * value.
 */
lfpug.model.ArrayCollection.prototype.getItemAt = function(index) {
  return this.source_[index];
};


/**
 * @return {!number} The length of the collection.
 */
lfpug.model.ArrayCollection.prototype.getLength = function() {
  return this.source_.length;
};


/**
 * @param {Object} item The item you wish to get the index.
 * @param {number=} opt_fromIndex The index at which to start the search. If
 *     omitted the search starts at index 0.
 * @return {!number} The length of the collection.
 */
lfpug.model.ArrayCollection.prototype.getItemIndex =
    function(item, opt_fromIndex) {
  return goog.array.indexOf(this.source_, item);
};


/**
 * This overrides the existing item in the collection, should not be confused
 * with addItemAt.
 * @param {lfpug.model.BaseModel} item The item you wish to set.
 * @param {number} index The index at which you wish to set the item.
 * @return {?lfpug.model.BaseModel} Returns the item.
 */
lfpug.model.ArrayCollection.prototype.setItemAt = function(item, index) {
  var oldItem;
  if (index >= 0 && index < this.source_.length) {
    oldItem = this.getItemAt(index);
    this.removeListeners_(this.childObservableEvents_, oldItem,
        this.childListenerFunc_);
    this.source_[index] = item;
    this.addListeners_(this.childObservableEvents_, item,
        this.childListenerFunc_);
    /** @type {lfpug.events.ArrayCollectionEvent} */
    var e = new lfpug.events.ArrayCollectionEvent(
        lfpug.events.ArrayCollectionEvent.EventType.SET, item);
    e.setOldItem(oldItem);
    this.dispatchEvent(e);
  }
  return item;
};


/**
 * @return {Array.<lfpug.model.BaseModel>}
 *        a copy of the collection source, note, this is not a deep copy .
 */
lfpug.model.ArrayCollection.prototype.getSource = function() {
  return this.source_.slice();
};


/**
 * @param {Function} f the sorting function.
 * @return {Array.<lfpug.model.BaseModel>} returns a copy of the sorted
 * underlying source array.
 */
lfpug.model.ArrayCollection.prototype.sort = function(f) {
  this.source_.sort(f);
  /**
   * @type {lfpug.events.ArrayCollectionEvent}
   */
  var e = new lfpug.events.ArrayCollectionEvent(
      lfpug.events.ArrayCollectionEvent.EventType.SORT, this);
  this.dispatchEvent(e);
  return this.getSource();
};


/**
 * Syncs a new array with the source array while dispatching all the necesary
 * move, add and remove events to keep view binding working.
 * @param {Array} array
 *        A array of items to sync to.
 */
lfpug.model.ArrayCollection.prototype.syncWithArray = function(array) {
  var len = array.length;
  var itemA, itemB, index;
  for (var i = 0; i < len; i++) {
    itemA = this.getItemAt(i);
    itemB = array[i];
    if (itemA !== itemB) {
      index = this.getItemIndex(itemB, i);
      if (index === -1) {
        this.addItemAt(itemB, i);
      } else {
        this.moveItem(index, i);
      }
    }
  }
  var a = this.getLength();
  while ((a--) > len) {
    this.removeItemAt(a);
  }
};


/**
 * @override
 *
 */
lfpug.model.ArrayCollection.prototype.disposeInternal = function() {
  //call super
  goog.base(this, 'disposeInternal');
  for (var i = 0; i < this.source_.length; i++) {
    var o = /**@type {lfpug.model.BaseModel}*/ this.source_[i];
    this.removeListeners_(this.childObservableEvents_, o,
        this.childListenerFunc_);
    //Note: We do not dispose of the model here, models should be disposed off
    // manually
  }
};


/**
 * @param {lfpug.events.ModelChangeEvent} event The child models change
 * event, this just gets dispatched up the chain again.
 * @private
 */
lfpug.model.ArrayCollection.prototype.propegateEventInternal_ =
    function(event) {

  var arr = event.getChain();
  arr.push(event);
  var e = new lfpug.events.ArrayCollectionEvent(
      lfpug.events.ArrayCollectionEvent.EventType.CHANGE, this, arr);
  this.dispatchEvent(e);
};


/**
 * @type {string}
 */
lfpug.model.ArrayCollection.prototype.id_;


/**
 * @return {string}
 *         This AC's unique ID.
 */
lfpug.model.ArrayCollection.prototype.getId = function() {
  return this.id_;
};


/**
 * @param {string} id
 *        The identifier label.
 */
lfpug.model.ArrayCollection.prototype.setId = function(id) {
  this.id_ = id;
};




